# -*- coding: utf-8 -*-

# Neomis -- A computer version of the well-known electronic game named Simon
#
# Copyright (C) 2009-2010 Valéry Febvre <vfebvre@easter-eggs.com>
# http://code.google.com/p/neomis/
#
# This file is part of Neomis.
#
# Neomis is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Neomis is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Neomis -- A computer version of the well-known electronic game named Simon
"""

import alsaaudio, wave
import cPickle, os.path, random
import ecore, elementary, evas

APP_VERSION = '1.0.3'
APP_NAME = 'Neomis'
DATA_DIR = '/usr/share/neomis/'
#DATA_DIR = '../data'

THEME_EDJ = os.path.join(DATA_DIR, 'neomis.edj')

DEFAULT_SETTINGS = {
    'fullscreen': False,
    'rotation':   0,
    'sounds':     True,
    }

BUTTONS = {
    'yellow': {'color': (222, 177,  11, 255), 'color_clicked': (255, 255,   0, 255), 'sound': 'yellow.wav'},
    'red':    {'color': (177,  11,  11, 255), 'color_clicked': (255,   0,   0, 255), 'sound': 'red.wav'},
    'green':  {'color': ( 11, 133,  55, 255), 'color_clicked': (  0, 255,   0, 255), 'sound': 'green.wav'},
    'blue':   {'color': ( 22,  99, 177, 255), 'color_clicked': (  0,   0, 255, 255), 'sound': 'blue.wav'},
}

#
# Config
#

class Config():
    def __init__(self):
        cfg_dir = os.path.join(os.path.expanduser("~"), ".config/%s" % APP_NAME.lower())
        if not os.path.exists(cfg_dir):
            os.makedirs(cfg_dir, 0755)
        
        self.cfg_path = os.path.join(cfg_dir, '%s.conf' % APP_NAME.lower())
        if not os.path.exists(self.cfg_path):
            self.data = DEFAULT_SETTINGS
        else:
            self.data = cPickle.load(open(self.cfg_path, 'r'))

        self.is_dirty = False

    def save(self):
        if self.is_dirty:
            cPickle.dump(self.data, open(self.cfg_path, 'w+'))
            self.is_dirty = False

    def get(self, name):
        if name not in self.data.keys():
            # a new setting not available in user settings yet
            self.data[name] = DEFAULT_SETTINGS[name]
        return self.data[name]

    def set(self, name, value):
        if self.data[name] != value:
            self.data[name] = value
            self.is_dirty = True

cfg = Config()


#
# Playground
#

class Playground(object):
    def __init__(self, main):
        self.main = main
        self.state = 'stop'
        self.timer_play_sequence = None
        self.timer_play_sequence_color = None

        self.layout = elementary.Layout(self.main.win)
        self.layout.file_set(THEME_EDJ, 'playground')
        self.layout.show()

        # 4 colored buttons: yellow, red, green, blue
        self.buttons_colors = {}
        btn = evas.Rectangle(self.main.win.evas)
        btn.name = 'button-yellow'
        btn.color = BUTTONS['yellow']['color']
        self.layout.content_set('button-yellow', btn)
        btn.on_mouse_down_add(self.on_button_color_clicked)
        self.buttons_colors['yellow'] = btn

        btn = evas.Rectangle(self.main.win.evas)
        btn.name = 'button-red'
        btn.color = BUTTONS['red']['color']
        self.layout.content_set('button-red', btn)
        btn.on_mouse_down_add(self.on_button_color_clicked)
        self.buttons_colors['red'] = btn

        btn = evas.Rectangle(self.main.win.evas)
        btn.name = 'button-green'
        btn.color = BUTTONS['green']['color']
        self.layout.content_set('button-green', btn)
        btn.on_mouse_down_add(self.on_button_color_clicked)
        self.buttons_colors['green'] = btn

        btn = evas.Rectangle(self.main.win.evas)
        btn.name = 'button-blue'
        btn.color = BUTTONS['blue']['color']
        self.layout.content_set('button-blue', btn)
        btn.on_mouse_down_add(self.on_button_color_clicked)
        self.buttons_colors['blue'] = btn

        # 4 control buttons: quit, settings, start/restart, high scores
        btn = elementary.Icon(self.main.win)
        btn.file_set(THEME_EDJ, 'neomis/settings')
        btn.callback_clicked_add(self.on_button_settings_clicked)
        self.layout.content_set('button-settings', btn)

        btn = elementary.Icon(self.main.win)
        btn.file_set(THEME_EDJ, 'neomis/high-scores')
        btn.callback_clicked_add(self.on_button_high_scores_clicked)
        self.layout.content_set('button-high-scores', btn)

        btn = elementary.Icon(self.main.win)
        btn.file_set(THEME_EDJ, 'neomis/quit')
        btn.callback_clicked_add(self.on_button_quit_clicked)
        self.layout.content_set('button-quit', btn)

        self.button_start = elementary.Icon(self.main.win)
        self.button_start.file_set(THEME_EDJ, 'neomis/start')
        self.button_start.callback_clicked_add(self.start)
        self.layout.content_set('button-start', self.button_start)

        # label
        self.label_counter = evas.Text(self.main.win.evas)
        self.label_counter.style_set(evas.EVAS_TEXT_STYLE_SOFT_OUTLINE)
        self.label_counter.color = (255, 0, 0, 255)
        self.label_counter.outline_color = (255, 255, 255, 255)
        self.label_counter.font_set('DejaVuSans', 36)
        self.layout.content_set('label-counter', self.label_counter)

        # leds
        self.leds = []
        led = evas.Rectangle(self.main.win.evas)
        self.layout.content_set('led-1', led)
        self.leds.append(led)

        led = evas.Rectangle(self.main.win.evas)
        self.layout.content_set('led-2', led)
        self.leds.append(led)

        led = evas.Rectangle(self.main.win.evas)
        self.layout.content_set('led-3', led)
        self.leds.append(led)
        self.update_leds_color((60, 60, 60, 255))

        self.main.pager_main.content_push(self.layout)

    def play_sound(self, sound):
        if not cfg.get('sounds'):
            return

        # without wave
        #f = open(os.path.join(DATA_DIR, 'sounds/%s' % sound), 'rb')
        #self.main.pcm.write(f.read())
        #f.close()

        f = wave.open(os.path.join(DATA_DIR, 'sounds/%s' % sound), 'rb')
        data = f.readframes(320)
        while data:
            self.main.pcm.write(data)
            data = f.readframes(320)
        f.close()

    def on_button_color_clicked(self, button, event):
        if self.state not in ('hplay', 'stop'):
            return
        self.light_button_color(button)

    def on_button_quit_clicked(self, icon):
        self.highlight_button(icon)
        self.main.quit()

    def on_button_settings_clicked(self, icon):
        self.highlight_button(icon)
        self.main.show_settings()

    def on_button_high_scores_clicked(self, icon):
        self.highlight_button(icon)
        self.main.show_high_scores()

    def highlight_button(self, icon):
        icon.color_set(255, 0, 0, 255)
        ecore.timer_add(0.25, self.unhighlight_button, icon)
        
    def unhighlight_button(self, icon):
        icon.color_set(255, 255, 255, 255)

    def light_button_color(self, button):
        color_name = button.name.split('-')[1]
        ecore.timer_add(0, self.play_sound, '%s.wav' % color_name)
        button.color = BUTTONS[color_name]['color_clicked']
        self.timer_unlight_button_color = ecore.timer_add(0.25, self.unlight_button_color, button)

        if self.state == 'hplay':
            self.sequence_played.append(color_name)
            self.label_counter.text_set("%02d" % len(self.sequence_played))
            if self.sequence_played == self.sequence:
                self.add_sequence_color()
                self.play_sequence_countdown()
            elif self.sequence_played != self.sequence[:len(self.sequence_played)]:
                ecore.timer_add(0.5, self.play_sound, 'lose.wav')
                self.stop()
                self.main.high_scores.check_new(len(self.sequence) - 1)

    def unlight_button_color(self, button):
        button.color = BUTTONS[button.name.split('-')[1]]['color']
        # return False to stop the ecore timer
        return False

    def start(self, icon):
        self.highlight_button(icon)
        if self.state != 'stop':
            self.stop()
            return
        self.sequence = []
        self.add_sequence_color()
        self.button_start.file_set(THEME_EDJ, 'neomis/stop')
        self.update_leds_color((255, 255, 255, 255))
        self.label_counter.text_set('00')
        self.play_sequence_countdown()

    def add_sequence_color(self):
        self.state = 'cplay'
        self.sequence.append(random.choice(BUTTONS.keys()))

    def stop(self):
        # delete play timers
        if self.timer_play_sequence:
            self.timer_play_sequence.delete()
            self.timer_play_sequence = None
        if self.timer_play_sequence_color:
            self.timer_play_sequence_color.delete()
            self.timer_play_sequence_color = None
        # reset leds colors
        self.update_leds_color((60, 60, 60, 255))
        # reset counter
        self.label_counter.text_set('')
        self.state = 'stop'
        self.button_start.file_set(THEME_EDJ, 'neomis/start')

    def play_sequence_countdown(self):
        self.counter_play_sequence_countdown = 2
        self.update_leds_color((255, 0, 0, 255))
        self.timer_play_sequence = ecore.timer_add(1, self.play_sequence)

    def play_sequence(self):
        self.leds[self.counter_play_sequence_countdown].color = (255, 255, 255, 255)
        if self.counter_play_sequence_countdown == 0:
            self.sequence_pointer = 0
            self.timer_play_sequence_color = ecore.timer_add(.5, self.play_sequence_color)
            return False
        else:
            self.counter_play_sequence_countdown -= 1
            return True

    def play_sequence_color(self):
        self.light_button_color(self.buttons_colors[self.sequence[self.sequence_pointer]])
        self.sequence_pointer += 1
        if self.sequence_pointer == len(self.sequence):
            self.state = 'hplay'
            self.sequence_played = []
            return False
        else:
            return True

    def promote(self):
        self.main.pager_main.content_promote(self.layout)

    def update_leds_color(self, color):
        for led in self.leds:
            led.color = color


# Settings
#

class Settings(object):
    def __init__(self, main):
        self.main = main
        self.box = None

    def build(self):
        self.box = elementary.Box(self.main.win)
        self.box.size_hint_align_set(-1, -1)
        self.box.show()

        scroller = elementary.Scroller(self.main.win)
        scroller.bounce_set(False, False)
        scroller.size_hint_weight_set(1.0, 1.0)
        scroller.size_hint_align_set(-1.0, -1.0)
        self.box.pack_end(scroller)
        scroller.show()

        box = elementary.Box(self.main.win)
        box.size_hint_weight_set(1, 1)
        scroller.content_set(box)
        box.show()

        # fullscreen
        self.tg_fullscreen = elementary.Toggle(self.main.win)
        self.tg_fullscreen.label_set("Fullscreen")
        self.tg_fullscreen.size_hint_align_set(-1, 0)
        self.tg_fullscreen.state_set(cfg.get('fullscreen'))
        self.tg_fullscreen.states_labels_set("Yes", "No")
        self.tg_fullscreen.callback_changed_add(self.toggle_fullscreen)
        box.pack_end(self.tg_fullscreen)
        self.tg_fullscreen.show()

        # rotate
        self.tg_rotate = elementary.Toggle(self.main.win)
        self.tg_rotate.label_set("Orientation")
        self.tg_rotate.size_hint_align_set(-1, 0)
        self.tg_rotate.state_set(cfg.get('rotation'))
        self.tg_rotate.states_labels_set("Landscape", "Portrait")
        self.tg_rotate.callback_changed_add(self.toggle_rotation)
        box.pack_end(self.tg_rotate)
        self.tg_rotate.show()

        # sounds
        self.tg_sounds = elementary.Toggle(self.main.win)
        self.tg_sounds.label_set("Sounds")
        self.tg_sounds.size_hint_align_set(-1, 0)
        self.tg_sounds.state_set(cfg.get('sounds'))
        self.tg_sounds.states_labels_set("Yes", "No")
        self.tg_sounds.callback_changed_add(self.toggle_sounds)
        box.pack_end(self.tg_sounds)
        self.tg_sounds.show()

        self.main.pager.content_push(self.box)

    def promote(self):
        if not self.box:
            self.build()
        self.main.pager.content_promote(self.box)

    def toggle_fullscreen(self, toggle):
        fullscreen = self.tg_fullscreen.state_get()
        if fullscreen != cfg.get('fullscreen'):
            self.main.win.fullscreen_set(fullscreen)
            cfg.set('fullscreen', fullscreen)

    def toggle_rotation(self, toggle):
        cfg.set('rotation', toggle.state_get())
        self.main.win.rotation_set(toggle.state_get() * 270)

    def toggle_sounds(self, toggle):
        cfg.set('sounds', toggle.state_get())


#
# High Scores
#

class HighScores(object):
    def __init__(self, main):
        self.main = main

        self.path = os.path.join(os.path.expanduser("~"), ".config/%s/high_scores" % APP_NAME.lower())
        if os.path.exists(self.path):
            self.data = cPickle.load(open(self.path, 'r'))
        else:
            self.data = []

        # main box
        self.box = elementary.Box(self.main.win)
        self.box.size_hint_align_set(-1, -1)
        self.box.show()

        # list
        self.list = elementary.List(self.main.win)
        self.list.size_hint_weight_set(1.0, 1.0)
        self.list.size_hint_align_set(-1.0, -1.0)
        self.box.pack_end(self.list)
        self.list.show()

        self.populate()

        self.main.pager.content_push(self.box)

    def check_new(self, score):
        high_scores = map(lambda h: h['score'], self.data)
        if score > 0 and (len(high_scores) < 10 or score > min(high_scores)):
            self.add(score)

    def add(self, score):
        self.iw = elementary.InnerWindow(self.main.win)
        self.iw.show()

        box = elementary.Box(self.main.win)
        self.iw.content_set(box)
        box.show()

        label_title = elementary.Label(self.main.win)
        label_title.label_set('<b>New High Score: %d</>' % score)
        box.pack_end(label_title)
        label_title.show()

        frame = elementary.Frame(self.main.win)
        frame.label_set("Enter Your Name")
        frame.size_hint_weight_set(1, 1)
        frame.size_hint_align_set(-1, 0.5)
        box.pack_end(frame)
        frame.show()

        box_name = elementary.Box(self.main.win)
        box_name.horizontal_set(True)
        frame.content_set(box_name)
        box_name.show()

        scroller_name = elementary.Scroller(self.main.win)
        scroller_name.size_hint_weight_set(1, 1)
        scroller_name.size_hint_align_set(-1, -1)
        scroller_name.bounce_set(True, False)
        box_name.pack_end(scroller_name)
        scroller_name.show()

        self.entry_name = elementary.Entry(self.main.win)
        self.entry_name.scale_set(2)
        self.entry_name.single_line_set(True)
        self.entry_name.size_hint_weight_set(1, 1)
        self.entry_name.size_hint_align_set(-1, -1)
        scroller_name.content_set(self.entry_name)
        self.entry_name.show()
        # FIXME: Hack
        button_hack = elementary.Button(self.main.win)
        box_name.pack_end(button_hack)
        button_hack.size_hint_min_set(0, button_hack.size_hint_min[1])
        button_hack.hide()

        box_buttons = elementary.Box(self.main.win)
        box_buttons.horizontal_set(True)
        box_buttons.homogenous_set(True)
        box_buttons.size_hint_align_set(-1.0, 0)
        box.pack_end(box_buttons)
        box_buttons.show()

        button = elementary.Button(self.main.win)
        button.size_hint_weight_set(1, 0)
        button.size_hint_align_set(-1, 0)
        button.label_set('Cancel')
        button.callback_clicked_add(self.cancel_add)
        box_buttons.pack_end(button)
        button.show()

        button = elementary.Button(self.main.win)
        button.size_hint_weight_set(1, 0)
        button.size_hint_align_set(-1, 0)
        button.label_set('OK')
        button.callback_clicked_add(self.validate_add, score)
        box_buttons.pack_end(button)
        button.show()

    def validate_add(self, button, score):
        name = self.entry_name.entry_get().replace('<br>', '')
        if not name:
            return
        self.data.append({'name': name, 'score': score, 'level': 1})
        self.data = sorted(self.data, key = lambda s : s['score'], reverse = True)[:10]
        cPickle.dump(self.data, open(self.path, 'w+'))
        self.populate()
        self.iw.delete()
        self.main.show_high_scores()

    def cancel_add(self, button):
        self.iw.delete()

    def promote(self):
        self.main.pager.content_promote(self.box)

    def populate(self, *args):
        self.list.clear()

        for hs in sorted(self.data, key = lambda s : s['score'], reverse = True):
            label = elementary.Label(self.main.win)
            label.scale_set(2)
            label.label_set(' %d ' % hs['score'])
            self.list.item_append(hs['name'], label, None, None, None)
        self.list.go()


#
# About
#

class About(object):
    def __init__(self, main):
        self.main = main
        self.box = None

    def build(self):
        # main box
        self.box = elementary.Box(self.main.win)
        self.box.size_hint_align_set(-1, -1)
        self.box.show()

        label = elementary.Label(self.main.win)
        label.label_set('<b>%s %s</>' % (APP_NAME, APP_VERSION))
        label.size_hint_align_set(0.5, 0.5)
        self.box.pack_end(label)
        label.show()

        scroller = elementary.Scroller(self.main.win)
        scroller.bounce_set(False, True)
        scroller.size_hint_weight_set(1.0, 1.0)
        scroller.size_hint_align_set(-1.0, -1.0)
        self.box.pack_end(scroller)
        scroller.show()

        box = elementary.Box(self.main.win)
        box.size_hint_weight_set(1, 1)
        scroller.content_set(box)
        box.show()

        about  = """\
<b>Neomis</> is a computer version of the well-known electronic game named <b>Simon</>.

It's a game of memory and concentration.

You must follow the pattern of sounds and lights as long as you can remember!

The game will flash quadrants in turn and expect you to repeat the sequence. If you get the sequence correct, the game will respond with a longer sequence.

<b>Copyright</> © 2009-2010 Valéry Febvre

<b>Licensed</> under the GNU GPL v3

<b>Homepage</> http://code.google.com/p/neomis/

If you like this program send me an email to vfebvre@easter-eggs.com\
"""

        entry = elementary.Entry(self.main.win)
        entry.editable_set(False)
        entry.line_wrap_set(True)
        entry.size_hint_align_set(-1, -1)
        entry.entry_set(about.replace('\n', '<br>'))
        box.pack_end(entry)
        entry.show()

        self.main.pager.content_push(self.box)

    def promote(self):
        if not self.box:
            self.build()
        self.main.pager.content_promote(self.box)


class Neomis(object):
    def __init__(self):
        self.pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, alsaaudio.PCM_NORMAL)
        self.pcm.setchannels(1)
        self.pcm.setrate(44100)
        self.pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
        #self.pcm.setperiodsize(160)

        self.win = elementary.Window(APP_NAME, elementary.ELM_WIN_BASIC)
        self.win.title_set(APP_NAME)
        self.win.fullscreen_set(cfg.get('fullscreen'))
        self.win.rotation_set(cfg.get('rotation') * 270)
        self.win.callback_destroy_add(self.quit)

        bg = elementary.Background(self.win)
        self.win.resize_object_add(bg)
        bg.size_hint_weight_set(1, 1)
        bg.show()

        self.pager_main = elementary.Pager(self.win)
        self.pager_main.size_hint_weight_set(1.0, 1.0)
        self.win.resize_object_add(self.pager_main)
        self.pager_main.show()

        self.box = elementary.Box(self.win)
        self.pager_main.content_push(self.box)
        self.box.show()

        toolbar = elementary.Toolbar(self.win)
        toolbar.menu_parent_set(self.win)
        toolbar.homogenous_set(False)
        toolbar.size_hint_align_set(-1.0, 0)
        self.box.pack_end(toolbar)
        toolbar.show()

        self.toolbar_item_playground = toolbar.item_append('neomis', "Playground", self.show_playground)
        self.toolbar_item_settings = toolbar.item_append('settings', "Settings", self.show_settings)
        self.toolbar_item_high_scores = toolbar.item_append('high-scores', "High Scores", self.show_high_scores)
        self.toolbar_item_about = toolbar.item_append('about', "About", self.show_about)

        self.pager = elementary.Pager(self.win)
        self.pager.size_hint_weight_set(1.0, 1.0)
        self.pager.size_hint_align_set(-1.0, -1.0)
        self.box.pack_end(self.pager)
        self.pager.show()

        self.settings = Settings(self)
        self.high_scores = HighScores(self)
        self.about = About(self)

        self.playground = Playground(self)
        self.show_playground()

        self.win.resize(480, 640)
        self.win.show()

    def quit(self, *args):
        self.pcm.close()
        # save settings
        cfg.save()
        # exit
        elementary.exit()

    def show_playground(self, *args):
        self.playground.promote()

    def show_settings(self, *args):
        self.toolbar_item_settings.selected = True
        self.pager_main.content_promote(self.box)
        self.settings.promote()

    def show_about(self, *args):
        self.about.promote()

    def show_high_scores(self, *args):
        self.toolbar_item_high_scores.selected = True
        self.pager_main.content_promote(self.box)
        self.high_scores.promote()


def main():
    elementary.init()
    elementary.theme_overlay_add(THEME_EDJ)
    Neomis()
    elementary.run()
    elementary.shutdown()
    return 0

if __name__ == "__main__":
    exit(main())
